Dit essay legt uit hoe Git werkt. Het gaat er van uit dat u Git goed genoeg begrijpt om het als versiecontrole-systeem voor uw projecten te gebruiken.

Het essay legt de nadruk op de graaf-structuur waarop Git is gebaseerd, en hoe de eigenschappen van deze graaf het gedrag van Git bepalen. Door naar de bouwstenen te kijken baseert u uw mentale model meer op de waarheid dan op hypotheses die voortkomen uit bewijs dat al experimenterend met de API is vergaard. Dit realistischer model verschaft u een beter begrip van wat Git heeft gedaan, wat het doet en wat het zal gaan doen.

De tekst is opgebouwd rond een serie Git-commando's, uitgevoerd op een enkel project. Deze worden afgewisseld met observaties over de graaf-datastructuur waarop Git is gebouwd. Deze observaties illustreren een eigenschap van de graaf, en het gedrag dat uit deze eigenschap voortkomt.

Als u naderhand misschien nog dieper in Git wilt duiken, kunt u een blik werpen op de uitgebreid becommentarieerde broncode van mijn implementatie van Git in JavaScript.

(Vaak zal ik termen die in verband met Git worden gebruikt vanaf nu onvertaald laten. Het wordt dus vaak "Tree Graph" ipv. "Boom Graaf" Hans)

Creëer het project

~ $ mkdir alpha
~ $ cd alpha

De gebruiker maakt alpha, een directory voor zijn/haar project.

~/alpha $ mkdir data
~/alpha $ printf 'a' > data/letter.txt

Hij/zij gaat naar de alpha directory en maakt een directory data aan.
(Gebruik onder Windows "echo a" ipv. "printf 'a'". Hans)
Daarin wordt een bestand letter.txt aangemaakt met de letter a als inhoud. De alpha directory ziet er nu aldus uit:

alpha
└── data
    └── letter.txt

Initialiseer de repository

~/alpha $ git init
          Initialized empty Git repository

git init maakt van de huidige directory een Git repository. Daarbij creëert het een .git directory en maakt daar enkele bestanden aan. Deze bestanden definiëren alles met betrekking tot de Git configuratie en de geschiedenis van het project. Het zijn doodgewone bestanden. De gebruiker kan ze lezen of editen met een teksteditor of shell. Oftewel: de gebruiker kan de geschiedenis van het project even gemakkelijk lezen en wijzigen als de bestanden van het project.

De alpha directory ziet er nu zo uit:

alpha
├── data
|   └── letter.txt
└── .git
    ├── objects
    etc...

De .git directory en de inhoud daarvan zijn van Git. Al de andere bestanden worden in het algemeen de werkkopie genoemd. Zij behoren aan de gebruiker toe.

Voeg een paar bestanden toe

~/alpha $ git add data/letter.txt

De gebruiker voert git add uit op data/letter.txt. Dit heeft twee gevolgen:

  • Ten eerste wordt er een nieuw blob bestand aangemaakt in de .git/objects/ directory.

    Dit blob bestand bevat de gecomprimeerde inhoud van data/letter.txt. De naam ervan is afgeleid van de gehashte inhoud. Hashing van een stuk tekst houdt in dat het wordt omgezet naar een kleiner1 stuk tekst dat het origineel uniek 2 identificeert.
    (Dus niet bevat zoals bij encryptie of compressie!!! Hans)
    Git hasht a bijvoorbeeld naar 2e65efe2a145dda7ee51d1741299f848e5bf752e. De eerste twee karakters worden gebruikt als naam van een directory in de objects-database: .git/objects/2e/. De rest van de hash wordt gebruikt als naam van het blob bestand dat de inhoud van de toegevoegde file bevat: .git/objects/2e/65efe2a145dda7ee51d1741299f848e5bf752e.

    Merk op dat alleen al door het toevoegen van een bestand aan Git de inhoud ervan wordt opgeslagen in de objects directory. Die inhoud bevindt zich nog steeds veilig in Git als de gebruiker data/letter.txt wist in de werkkopie.

  • Ten tweede voegt git add het bestand toe aan de index. De index is een lijst met alle bestanden die Git geacht wordt in de gaten te houden ("being tracked"). Die lijst is opgeslagen als een bestand in .git/index. Elke regel van het bestand verbindt een beheerd bestand met de hash ("maps a tracked file to the hash") van de inhoud ervan op het moment dat het werd toegevoegd. Dit is de index nadat het git add -commando is uitgevoerd:

    data/letter.txt 2e65efe2a145dda7ee51d1741299f848e5bf752e
    

Nu maakt de gebruiker een bestand data/number.txt aan met 1234 als inhoud:

~/alpha $ printf '1234' > data/number.txt

De werkkopie ziet er nu zo uit:

alpha
└── data
    └── letter.txt
    └── number.txt

De gebruiker voegt het bestand aan Git toe:

~/alpha $ git add data

Het git add commando creëert een blob object dat de inhoud van data/number.txt bevat. En het voegt een entry in de index toe voor data/number.txt die naar de blob verwijst. Dit is de index na het voor de tweede keer uitvoeren van het git add commando:

data/letter.txt 2e65efe2a145dda7ee51d1741299f848e5bf752e
data/number.txt 274c0052dd5408f8ae2bc8440029ff67d79bc5c3

Merk op dat alleen de bestanden in de data directory zijn opgenomen in de index, hoewel de gebruiker git add data uitvoerde. De data directory is niet afzonderlijk opgenomen.

~/alpha $ printf '1' > data/number.txt
~/alpha $ git add data

Stel dat toen de gebruiker data/number.txt aanmaakte, het eigenlijk de bedoeling was 1 te typen, niet 1234. De correctie wordt aangebracht en het bestand opnieuw aan de index toegevoegd. Dit commando creëert een nieuwe blob met de nieuwe inhoud. En de entry in de index wordt geupdate zodat deze voor data/number.txt naar de nieuwe blob verwijst.

Het maken van een commit

~/alpha $ git commit -m 'a1'
          [master (root-commit) 774b54a] a1

De gebruiker maakt de commit a1. Git drukt wat gegevens over de commit af. Deze gegevens zullen snel duidelijk worden.

Het commit-commando heeft drie gevolgen.

  • Er wordt een "tree graph" aangemaakt die de inhoud van de versie van het project dat wordt gecommit vertegenwoordigt.
  • Het maakt een commit-object aan.
  • Het laat de huidige branch wijzen naar het nieuwe commit-object.

Het aanmaken van een tree graph

Git legt de huidige staat van het project vast door een tree graph te maken op basis van de index. Deze tree graph slaat de locatie en inhoud van elk bestand van het project op.

De graph bevat twee soorten objecten: blobs en trees.

  • Blobs worden opgeslagen door git add. Zij vertegenwoordigen de inhoud van bestanden.
  • Trees worden opgeslagen bij een commit. Een tree vertegenwoordigt een directory in de werkkopie.

Hier volgt het tree-object dat de inhoud van de data directory vastlegt voor de nieuwe commit:

100664 blob 2e65efe2a145dda7ee51d1741299f848e5bf752e letter.txt
100664 blob 56a6051ca2b02b04ef92d5150c9ef600403cb1de number.txt

De eerste regel legt alles vast wat nodig is om data/letter.txt te reproduceren.

  • Het eerste deel geeft de file permissies weer.
  • Het tweede deel geeft aan dat de inhoud van deze entry door een blob wordt vertegenwoordigt, en niet door een tree.
  • Het derde deel geeft de hash van de blob.
  • Het vierde deel is de filenaam.

De tweede regel geeft dezelfde informatie voor data/number.txt.

Hier is het tree-object voor alpha, de root-directory van het project:

040000 tree 0eed1217a2947f4930583229987d90fe5e8e0b74 data

De enige regel in deze tree wijst naar de data tree.

Tree graph for the `a1` commit

Tree graph voor de `a1` commit

In de graph hierboven wijst de root tree naar de data tree. De data tree wijst naar de blobs voor data/letter.txt en data/number.txt.

Het aanmaken van een commit-object

Na de tree graph te hebben gemaakt creëert git commit een commit-object. Het commit-object is gewoon een ander tekstbestand in .git/objects/:

tree ffe298c3ce8bb07326f888907996eaa48d266db4
author Mary Rose Cook <mary@maryrosecook.com> 1424798436 -0500
committer Mary Rose Cook <mary@maryrosecook.com> 1424798436 -0500

a1

De eerste regel wijst naar de tree graph. De hash is van het tree-object dat de root van de werkkopie vertegenwoordigt. Dat is de alpha directory. De laatste regel is de commit-message.

`a1` commit object pointing at its tree graph

Het `a1` commit-object dat naar zijn tree graph wijst

De huidige branch naar het nieuwe commit-object laten wijzen

Tenslotte laat het commit-commando de huidige branch naar het nieuwe commit-object wijzen.

Wat is de huidige branch? Git kijkt in het HEAD bestand in .git/HEAD en vindt:

ref: refs/heads/master

Dit zegt dat HEAD naar master wijst. master is de huidige branch.

HEAD en master zijn allebei "refs". Een ref is een label dat door Git of de gebruiker wordt gebruikt om een specifieke commit te identificeren.

Het bestand dat de master ref representeert bestaat niet, omdat dit de eerste commit naar de repository is. Git creëert het bestand in .git/refs/heads/master met als inhoud de hash van het commit-object:

74ac3ad9cde0b265d2b4f1c778b283a6e2ffbafd

(Wanneer u deze Git commando's al lezend intypt, zal de hash van uw a1 commit verschillen van de mijne. Content-objecten als blobs en trees zullen altijd dezelfde hash opleveren. Commits niet, omdat ze ook datums en de namen van hun makers bevatten.)

Laten we HEAD en master toevoegen aan de Git graph:

`master` pointing at the `a1` commit

`HEAD` wijzend naar `master` en `master` wijzend naar de `a1` commit

HEAD wijst naar master, zoals het dat voor de commit al deed. Maar master bestaat nu en wijst naar het nieuwe commit-object.

Het maken van een commit die niet de eerste commit is

Hier ziet u de Git graph na de a1 commit. De werkkopie en index zijn ook opgenomen.

`a1` commit shown with the working copy and index

De `a1` commit, getoond met werkkopie en index

Merk op dat de werkkopie, index, en a1 commit allemaal dezelfde inhoud hebben voor data/letter.txt en data/number.txt. De index en HEAD commit gebruiken beide hashes om naar blob objecten te verwijzen, maar de inhoud van de werkkopie is als tekst opgeslagen op een andere lokatie.

~/alpha $ printf '2' > data/number.txt

De gebruiker verandert nu de inhoud van data/number.txt in 2. Dit verandert de werkkopie, maar laat de index en de HEAD commit zoals ze waren.

`data/number.txt` set to `2` in the working copy

`data/number.txt` veranderd in `2` in de werkkopie

~/alpha $ git add data/number.txt

De gebruiker voegt het bestand toe aan Git. Git voegt een blob met 2 als inhoud toe aan de objects directory. En het laat de index entry voor data/number.txt naar de nieuwe blob wijzen.

`data/number.txt` set to `2` in the working copy and index

`data/number.txt` gewijzigd in `2` in de werkkopie en index

~/alpha $ git commit -m 'a2'
          [master f0af7e6] a2

De gebruiker commit. De stappen voor de commit zijn hetzelfde als eerst:

  • Als eerste wordt er een nieuwe tree graph aangemaakt die de inhoud van de index vertegenwoordigt.

    De index entry voor data/number.txt is veranderd. De oude data tree geeft niet langer de geïndexeerde toestand van de data directory weer. Er moet een nieuw data tree object worden gecreëerd:

    100664 blob 2e65efe2a145dda7ee51d1741299f848e5bf752e letter.txt
    100664 blob d8263ee9860594d2806b0dfd1bfd17528b0ba2a4 number.txt
    

    De nieuwe data tree geeft een andere hash-waarde dan de oude data tree. Er moet een nieuwe root tree worden gecreëerd om deze hash op te slaan:

    040000 tree 40b0318811470aaacc577485777d7a6780e51f0b data
    
  • Als tweede wordt er een nieuw commit-object aangemaakt.

    tree ce72afb5ff229a39f6cce47b00d1b0ed60fe3556
    parent 774b54a193d6cfdd081e581a007d2e11f784b9fe
    author Mary Rose Cook <mary@maryrosecook.com> 1424813101 -0500
    committer Mary Rose Cook <mary@maryrosecook.com> 1424813101 -0500
    
    a2
    

    De eerste regel van het commit-object wijst naar het nieuwe root tree object.
    De tweede verwijst naar a1: de parent van de commit. Om de parent van de commit te vinden is Git naar HEAD gegaan, volgde die naar master en vond daar de commit hash van a1.

  • Als derde wordt de inhoud van de master branch file gewijzigd in de hash van de nieuwe commit.

`a2` commit

`a2` commit

Git graph without the working copy and index

Git graph zonder de werkkopie en index

Graph eigenschap I.: Inhoud wordt als een object-tree opgeslagen. Dit betekent dat alleen diffs in de objects-database worden opgeslagen.
Bekijk bijvoorbeeld de bovenstaande graph. De a2 commit hergebruikt de a blob die eerder dan de a1 commit was aangemaakt. Evenzo kunnen, als een hele directory van commit tot commit niet verandert, de tree ervan en alle blobs en trees eronder worden hergebruikt. In het algemeen zijn er van commit tot commit weinig inhoudelijke veranderingen. Zodoende kan Git grote commit-histories opslaan in een kleine ruimte.

Graph eigenschap II.: Elke commit heeft een parent. Zo kan een repository de geschiedenis van een project opslaan.

Graph eigenschap III.: Refs zijn toegangspunten tot het een of andere gedeelte van de commit historie. Dus kunnen er aan commits betekenisvolle namen worden gegeven. De gebruiker organiseert zijn/haar werk in afstammingslijnen (lineage) die in het project betekenisvol zijn, zoals met concrete refs als fix-for-bug-376. Git gebruikt symbolische refs als HEAD, MERGE_HEAD en FETCH_HEAD ter ondersteuning van commando's die de commit historie manipuleren.

Graph eigenschap IV.: De nodes in de objects/ directory zijn immutable. Dit betekent dat inhoud wordt aangepast, niet gewist. Elk stuk informatie dat ooit is toegevoegd en elke commit die ooit is gedaan kan ergens in de objects directory3 worden teruggevonden.

Graph eigenschap V.: Refs zijn mutable. Daarom kan de betekenis van een ref veranderen. De commit waar master naar wijst kan de beste versie zijn van een project op dit moment, maar die positie kan binnen de kortste keren worden ingenomen door een nieuwere en betere commit.

Graph eigenschap VI.: De werkkopie en de commits waar door refs naar wordt gewezen zijn direkt beschikbaar, maar andere commits zijn dat niet. Dit betekent dat recente geschiedenis gemakkelijk kan worden opgeroepen, maar ook dat die vaker verandert. Oftewel: Git heeft een vervagend geheugen dat met steeds moeizamer middelen moet worden opgeroepen.

De werkkopie is het punt in de historie dat het gemakkelijkst is terug te vinden omdat het zich in de root van de repository bevindt. Er is niet eens een Git commando voor nodig. Maar het is ook het veranderlijkste punt in de geschiedenis. De gebruiker kan tientallen versies van een bestand maken zonder dat Git er een van opslaat, tenzij ze aan Git worden toegevoegd.

De commit waar HEAD naar wijst is erg gemakkelijk op te roepen. Deze is aan de basis van de branch die is uitgecheckt. Om de inhoud ervan te zien hoeft de gebruiker alleen maar te "stashen"4 en dan de werkkopie te bekijken. Tegelijkertijd is HEAD de meest veranderlijke ref.

Ook de commit waar een concrete ref naar verwijst is gemakkelijk op te roepen. de gebruiker kan die branch eenvoudig uitchecken. The top van een branch verandert minder vaak dan HEAD, maak vaak genoeg om de betekenis van een branchnaam veranderlijk te laten zijn.

Het is moeilijk om een commit waar door geen enkele ref naar wordt verwezen terug te roepen. Hoe verder de gebruiker verwijdert raakt van een ref, des te moeilijker het voor hem/haar wordt de betekenis van een commit te reconstrueren. Maar ook, hoe verder hij/zij teruggaat, des te minder waarschijnlijk het is dat de een of ander de geschiedenis zal hebben veranderd sinds de vorige keer dat hij/zij keek5.

Het uitchecken van een commit

~/alpha $ git checkout 37888c2
          You are in 'detached HEAD' state...

De gebruiker checkt de a2 commit uit door de hash ervan te gebruiken.
(Als u deze Git commando's uitvoert, zal deze niet werken. Gebruik git log om de hash van uw a2 commit te vinden.)

Uitchecken gaat in vier stappen:

  • Ten eerste krijgt Git de a2 commit en ook de tree graph waar die naar wijst.

  • Ten tweede schrijft het de file entries in de tree graph naar de werkkopie. Dit levert geen veranderingen op. De werkkopie heeft de inhoud van de tree graph al die er naartoe is geschreven omdat HEAD via master al naar de a2 commit wees.

  • Ten derde schrijft Git de file entries in de tree graph naar de index. Ook dit resulteert niet in veranderingen. De index heeft de inhoud van de a2 commit al.

  • Ten vierde wordt de inhoud van HEAD gewijzigd in de hash van de a2 commit:

    f0af7e62679e144bb28c627ee3e8f7bdb235eee9
    

    Het wijzigen van de inhoud van HEAD in een hash brengt de repository in the "detached HEAD" toestand. Merk bij de hier volgende graph op dat HEAD direct naar de a2 commit wijst, in plaats van naar master te wijzen.

    Detached `HEAD` on `a2` commit

    Detached `HEAD` na `a2` commit
    ~/alpha $ printf '3' > data/number.txt
    ~/alpha $ git add data/number.txt
    ~/alpha $ git commit -m 'a3'
              [detached HEAD 3645a0e] a3
    

Na het uitchecken verandert de gebruiker de inhoud van data/number.txt in 3 en commit de verandering. Git gaat naar HEAD om de parent van de a3 commit te krijgen. Inplaats van een branch-ref te vinden en te volgen, vindt en retourneert het de hash van de a2 commit.

Git past HEAD zodanig aan dat deze direct naar de hash van de nieuwe a3 commit wijst. De repository bevindt zich nog steeds in de detached HEAD toestand. De nieuwe commit bevindt zich niet op een branch omdat er geen enkele commit wijst naar of a3 of naar een van diens afstammelingen. Daarom kan deze gemakkelijk kwijtraken.

Van nu af aan zullen trees en blobs over het algemeen worden weggelaten bij de graph diagrammen.

`a3` commit that is not on a branch

`a3` commit die zich niet op een branch bevindt

Het maken van een branch

~/alpha $ git branch deputy

De gebruiker creëert een nieuwe branch met de naam deputy. Dit maakt gewoon een nieuw bestand aan in .git/refs/heads/deputy dat de hash bevat waar HEAD naar wijst: de hash van de a3 commit.

Graph property: branches zijn gewoon refs en refs zijn gewoon bestanden. Git branches zijn dus lichtgewicht.

Het aanmaken van de deputy branch plaatst de nieuwe a3 commit veilig op een branch. HEAD is nog steeds "detached" omdat deze nog steeds direct naar een commit verwijst.

`a3` commit now on the `deputy` branch

De `a3` commit bevindt zich nu op de `deputy` branch

Het uitchecken van een branch

~/alpha $ git checkout master
          Switched to branch 'master'

De gebruiker checkt de master branch uit:

  • Eerst krijgt Git de a2 commit waar masternaar wijst, en krijgt de tree graph waar de commit points naar verwijst.

  • Dan, als tweede, schrijft Git de file entries in de tree graph naar de bestanden van de werkkopie. De inhoud van data/number.txt wordt hierdoor 2.

  • Ten derde schrijft Git de file entries in de tree graph naar de index. Dit updatet de entry voor data/number.txt naar de hash van de 2 blob.

  • Als vierde laat Git HEAD naar master wijzen door de inhoud ervan te veranderen van een hash naar:

    ref: refs/heads/master
    

Het uiteindelijke resultaat:

`master` checked out and pointing at the `a2` commit

`master` uitgecheckt en wijzend naar de `a2` commit

Het uitchecken van een branch die incompatibel is met de werkkopie

~/alpha $ printf '789' > data/number.txt
~/alpha $ git checkout deputy
          Your changes to these files would be overwritten
          by checkout:
            data/number.txt
          Commit your changes or stash them before you
          switch branches.

De gebruiker verandert de inhoud van data/number.txt per ongeluk in 789. Daarna wordt geprobeerd deputy uit te checken. Git voorkomt het uitchecken.

HEAD wijst naar master die naar a2 verwijst waar data/number.txt als inhoud 2 heeft. deputy wijst naar a3 waar data/number.txt als inhoud 3 heeft. De werkkopie versie van data/number.txt heeft als inhoud 789. Al deze versies zijn verschillend, en de verschillen moeten worden opgelost.

Git zou de werkkopie versie van data/number.txt kunnen vervangen door de versie in de commit die wordt uitgecheckt. Maar het vermijdt dataverlies tegen elke prijs.

Git zou de werkkopie versie kunnen samenvoegen ("mergen") met de versie die wordt uitgecheckt. Maar dat is ingewikkeld.

Dus Git breekt de check out af.

~/alpha $ printf '2' > data/number.txt
~/alpha $ git checkout deputy
          Switched to branch 'deputy'

De gebruiker heeft nu door dat hij/zij per ongeluk data/number.txt heeft gewijzigd, en verandert de inhoud terug naar 2. deputy wordt nu succesvol uitgecheckt.

`deputy` checked out

`deputy` uitgecheckt

Merge met een voorganger (ancestor)

~/alpha $ git merge master
          Already up-to-date.

De gebruiker voegt master samen met deputy. Mergen van twee branches betekent het mergen van twee commits. De eerste commit is die waar deputy naar wijst: de ontvanger (receiver). De tweede commit is die waar master naar wijst: de donor (giver). In dit geval doet Git niets bij de merge. Het meldt dat het Already up-to-date. is.

Graph property: de serie commits in de graph wordt opgevat als een serie veranderingen uitgevoerd op de inhoud van de repository. Dit houdt in dat bij een merge, wanneer de giver commit een ancestor van de receiver commit is, Git niets zal doen. De veranderingen zijn al verdisconteerd.

Merge met een afstammeling (descendent)

~/alpha $ git checkout master
          Switched to branch 'master'

De gebruiker checkt master uit.

`master` checked out and pointing at the `a2` commit

`master` uitgecheckt en wijzend naar de `a2` commit

~/alpha $ git merge deputy
          Fast-forward

deputy wordt samengevoegd met master. Git ontdekt dat de receiver commit, a2, een ancestor is van de giver commit, a3. Het kan een fast-forward merge uitvoeren.

Het gaat naar de giver commit en krijgt de tree graph waar die naar wijst. Het schrijft de file entries in de tree graph naar de werkkopie en de index. Het “fast-forwards” master door die naar a3 te laten wijzen.

`a3` commit from `deputy` fast-forward merged into `master`

`a3` commit die van `deputy` fast-forward in `master` wordt gemerged

Graph property: de serie commits in the graph wordt beschouwd als een serie veranderingen uitgevoerd op de inhoud van de repository. Dit betekent dat bij een merge, wanneer de giver een descendent van de receiver is, de geschiedenis niet veranderd is. Er is al een serie commits die de te maken verandering beschrijft: de serie commits tussen de receiver en de giver. Maar hoewel de Git historie niet verandert, doet de Git graph dat wel. De concrete ref waar HEAD naar wijst wordt geupdate zodat die nu naar de giver commit wijst.

Merge van twee commits van verschillende afstamming (lineages)

~/alpha $ printf '4' > data/number.txt
~/alpha $ git add data/number.txt
~/alpha $ git commit -m 'a4'
          [master 7b7bd9a] a4

De gebruiker verandert nu de inhoud van number.txt in 4 en commit de verandering naar master.

~/alpha $ git checkout deputy
          Switched to branch 'deputy'
~/alpha $ printf 'b' > data/letter.txt
~/alpha $ git add data/letter.txt
~/alpha $ git commit -m 'b3'
          [deputy 982dffb] b3

De gebruiker checkt deputy uit. De inhoud van data/letter.txt wordt veranderd in b, en de verandering wordt naar deputy gecommit.

`a4` committed to `master`, `b3` committed to `deputy` and `deputy` checked out

`a4` gecommit naar `master`, `b3` gecommit naar `deputy`, en `deputy` uitgecheckt

Graph property: commits kunnen voorgangers (parents) delen. Zodoende kunnen nieuwe afstammingslijnen (lineages) in de commit historie worden gecreëerd.

Graph property: commits kunnen meerdere ouders (parents) hebben. Zo kunnen gescheiden afstammingslijnen (lineages) door een commit met twee ouders worden verenigd (joined): een merge commit.

~/alpha $ git merge master -m 'b4'
          Merge made by the 'recursive' strategy.

De gebruiker voegt master toe in deputy.

Git stelt vast dat de ontvanger (receiver), b3, en de donor (giver), a4, zich in verschillende afstammingslijnen (lineages) bevinden. Het maakt een merge commit. Dit proces heeft acht stappen:

  • Stap een A, Git schrijft de hash van de giver commit naar een bestand alpha/.git/MERGE_HEAD. De aanwezigheid van dit bestand vertelt Git dat het midden in het merge proces zit.

  • Stap twee A, Git vindt de basis (base) commit: de meest recente voorouder die de receiver en giver commits gemeenschappelijk hebben.

    `a3`, the base commit of `a4` and `b3`

    `a3`, de base commit van `a4` en `b3`

    Graph property: commits hebben parents. Dit betekent dat er een punt gevonden kan worden waar de twee afstammingslijnen zich splitsten. Git zoek terug in de tijd van b3 om alle voorouders daarvan te vinden, en terug in de tijd van a4 om alle voorouders daarvan te vinden. De meest recente voorouder die beide afstammingslijnen delen, a3, wordt de base commit.

  • Stap drie A, Git genereert the indices voor de base, receiver en giver commits vanuit hun tree graphs.

  • Stap vier A, Git genereert een diff die de veranderingen combineert die bij de base commit zijn aangebracht door de receiver commit en de giver commit. Deze diff is een lijst bestandspaden die naar een verandering wijzen: add, remove, modify of conflict.

    Git verzamelt de lijst van al de bestanden die voorkomen in de base, receiver of giver indices. Voor elk daarvan vergelijkt het de index entries om te beslissen wat er aan het bestand moet worden veranderd. Het schrijft een corresponderende entry naar de diff. In dit geval heeft de diff twee entries.

    De eerste entry is die voor data/letter.txt. De inhoud van dit bestand a in de base, b in de receiver en a in de giver. De inhoud verschilt tussen de base en de receiver. Maar identiek tussen de base en de giver. Git ziet dat de inhoud door de receiver is veranderd, maar niet door de giver. De diff entry voor data/letter.txt is een modificatie, niet een conflict.

    De tweede entry in the diff is die voor data/number.txt. In dit geval is de inhoud tussen base en receiver gelijk, maar verschillend in de giver. De diff entry voor data/letter.txt is ook een modificatie.

    Graph property: het is mogelijk de base commit van een merge te vinden. Dit betekent dat wanneer een bestand ten opzichte van de base alleen veranderd is in of de receiver of de giver, Git de merge van dat bestand automatisch kan oplossen. Dit bespaart de gebruiker werk dat hij/zij zou moeten doen.

  • Stap vijf A, de veranderingen die door de entries in de diff worden aangegeven worden doorgevoerd op de werkkopie. De inhoud van data/letter.txt wordt b, de inhoud van data/number.txt wordt veranderd in 4.

  • Stap zes A, de veranderingen die door de entries in the diff worden aangegeven worden toegepast op de index. De entry voor data/letter.txt verwijst nu naar de b blob en de entry voor data/number.txt wijst naar de 4 blob.

  • Stap zeven A, de geupdate index wordt gecommit:

    tree 20294508aea3fb6f05fcc49adaecc2e6d60f7e7d
    parent 982dffb20f8d6a25a8554cc8d765fb9f3ff1333b
    parent 7b7bd9a5253f47360d5787095afc5ba56591bfe7
    author Mary Rose Cook <mary@maryrosecook.com> 1425596551 -0500
    committer Mary Rose Cook <mary@maryrosecook.com> 1425596551 -0500
    
    b4
    

    Merk op dat de commit twee parents heeft.

  • Stap acht A, Git laat de huidige branch, deputy, nu naar de nieuwe commit wijzen.

    `b4`, the merge commit resulting from the recursive merge of `a4` into `b3`

    `b4`, de merge commit die voortkomt uit de recursieve merge van `a4` in `b3`

Het mergen van twee commits van verschillende afstammingslijnen (lineages) die beide het zelfde bestand modificeren

~/alpha $ git checkout master
          Switched to branch 'master'
~/alpha $ git merge deputy
          Fast-forward

De gebruiker checkt master uit. deputy wordt in master gemergd. Dit "fast-forwards" master naar de b4 commit. master en deputy wijzen nu naar dezelfde commit.

`deputy` merged into `master` to bring `master` up to the latest commit, `b4`

`deputy` in `master` gemergd, waarbij `master` gericht wordt op de laatste commit, `b4`

~/alpha $ git checkout deputy
          Switched to branch 'deputy'
~/alpha $ printf '5' > data/number.txt
~/alpha $ git add data/number.txt
~/alpha $ git commit -m 'b5'
          [deputy bd797c2] b5

De gebruiker checkt deputy uit. De inhoud van data/number.txt wordt veranderd in 5, en de verandering naar deputy gecommit.

~/alpha $ git checkout master
          Switched to branch 'master'
~/alpha $ printf '6' > data/number.txt
~/alpha $ git add data/number.txt
~/alpha $ git commit -m 'b6'
          [master 4c3ce18] b6

De gebruiker checkt master uit. De inhoud van data/number.txt wordt veranderd in 6, en de verandering naar master gecommit.

`b5` commit on `deputy` and `b6` commit on `master`

`b5` commit naar `deputy` en `b6` commit naar `master`
~/alpha $ git merge deputy
          CONFLICT in data/number.txt
          Automatic merge failed; fix conflicts and
          commit the result.

De gebruiker merget deputy in master. Er is een conflict en de merge wordt afgebroken.
Het proces bij een conflicterende merge volgt dezelfde eerste zes stappen als het proces voor een conflictloze merge: maak .git/MERGE_HEAD aan, vind de base commit, genereer the indices voor de base, receiver en giver commits, creëer een diff, update de werkkopie en update de index.
Vanwege het conflict worden de zevende commitstap en achtste ref-update-stap nooit genomen. Laten we de acht stappen nog eens doornemen en zien wat er gebeurt:

  • Stap een B, Git schrijft de hash van de donor (giver) commit naar een bestand .git/MERGE_HEAD.

    `MERGE_HEAD` written during merge of `b5` into `b6`

    `MERGE_HEAD`, geschreven tijdens de merge van `b5` in `b6`
  • Stap twee B, Git vindt the base commit, b4.

  • Stap drie B, Git genereert de indices voor de base, receiver en giver commits.

  • Stap vier B, Git genereert een diff die de veranderingen gemaakt in de base door de receiver commit en de giver commit combineert. Deze diff is een lijst van bestandspaden die naar een verandering wijzen: add, remove, modify of conflict.

    In dit geval bevat de diff slechts een entry: data/number.txt. Deze entry is gemarkeerd als een conflict, omdat de inhoud van data/number.txt verschillend is in de receiver, giver en base.

  • Stap vijf B, de veranderingen aangegeven door de entries in de diff worden toegepast op de werkkopie. In het geval van een conflicterend onderdeel, worden beide versies door Git naar het bestand in de werkkopie geschreven. De inhoud van data/number.txt wordt dan:

    <<<<<<< HEAD
    6
    =======
    5
    >>>>>>> deputy
    
  • Stap zes B, de veranderingen die door de entries in de diff worden aangegeven worden toegepast op de index. Entries in de index worden uniek geïdenticeerd door een combinatie van hun bestandspad en wachtcode (stage). De entry voor een conflictvrij bestand heeft een wachtcode 0. Voor deze merge zag de index er aldus uit (de 0-len zijn wachtcodes (stage values):

    (Git kent een "staging area", waar u bestanden klaar zet om te worden gecommit. Ik vertaal dat met "in de wacht zetten". Hans)

    0 data/letter.txt 63d8dbd40c23542e740659a7168a0ce3138ea748
    0 data/number.txt 62f9457511f879886bb7728c986fe10b0ece6bcb
    

    Nadat de merge diff naar de index is geschreven ziet deze de index er zo uit:

    0 data/letter.txt 63d8dbd40c23542e740659a7168a0ce3138ea748
    1 data/number.txt bf0d87ab1b2b0ec1a11a3973d2845b42413d9767
    2 data/number.txt 62f9457511f879886bb7728c986fe10b0ece6bcb
    3 data/number.txt 7813681f5b41c028345ca62a2be376bae70b7f61
    

    De entry voor data/letter.txt met wachtcode 0 is hetzelfde als deze voor de merge was. De entry voor data/number.txt met wachtcode 0 is verdwenen. Er zijn drie nieuwe entries voor in de plaats gekomen.
    De entry met wachtcode 1 bevat de hash van de base data/number.txt inhoud.
    De entry met wachtcode 2 bevat de hash van de receiver data/number.txt inhoud.
    De entry met wachtcode 3 bevat de hash van de giver data/number.txt inhoud.
    De aanwezigheid van deze drie entries vertelt Git dat er voor data/number.txt een conflict bestaat.

    De merge wordt onderbroken.

    ~/alpha $ printf '11' > data/number.txt
    ~/alpha $ git add data/number.txt
    

    De gebruiker integreert de inhoud van de twee conflicterende versies door de inhoud van data/number.txt te veranderen in 11. Het bestand wordt aan de index toegevoegd. Git voegt een blob toe met als inhoud 11. Het toevoegen van een conflicterend bestand is voor Git het teken dat het conflict is opgelost. Git verwijdert de data/number.txt entries met wachtcodes 1, 2 en 3 uit de index. Het voegt een entry toe voor data/number.txt met wachtcode 0, met de hash van de nieuwe blob als inhoud. De index ziet er nu uit als:

    0 data/letter.txt 63d8dbd40c23542e740659a7168a0ce3138ea748
    0 data/number.txt 9d607966b721abde8931ddd052181fae905db503
    

    ~/alpha $ git commit -m 'b11'
              [master 251a513] b11
    
  • Stap zeven B, de gebruiker commit. Git ziet .git/MERGE_HEAD in de repository, waardoor het weet dat er een merge aan de gang is. Het checkt de index en vindt dat er geen conflicten meer zijn. Het creëert een nieuwe commit, b11, om de inhoud van de opgeloste merge vast te leggen. Het wist het bestand .git/MERGE_HEAD. Dit completeert de merge.

  • Stap acht B, Git laat de huidige branch, master, naar de nieuwe commit wijzen.

    `b11`, the merge commit resulting from the conflicted, recursive merge of `b5` into `b6`

    `b11`, de merge commit als resultaat van de conflicterende, recursieve merge van `b5` in `b6`

Het verwijderen van een bestand

Dit diagram van de Git graph bevat zowel commit historie, de trees en blobs voor de laatste commit, als de werkkopie en index:

The working copy, index, `b11` commit and its tree graph

De werkkopie, index, `b11` commit en tree graph

~/alpha $ git rm data/letter.txt
          rm 'data/letter.txt'

De gebruiker vraagt Git data/letter.txt te verwijderen. Het bestand wordt verwijderd uit de werkkopie. De entry wordt gewist uit de index.

After `data/letter.txt` `rm`ed from working copy and index

Na het verwijderen van `data/letter.txt` (`rm`) uit werkkopie en index

~/alpha $ git commit -m '11'
          [master d14c7d2] 11

De gebruiker commit. Als onderdeel van de commit bouwt Git als altijd een tree graph op die de inhoud van de index representeert. data/letter.txt is niet opgenomen in de tree graph omdat het niet meer in de index staat.

`11` commit made after `data/letter.txt` `rm`ed

De `11` commit na de verwijdering van `data/letter.txt`

Het kopiëren van een repository

~/alpha $ cd ..
      ~ $ cp -R alpha bravo

De gebruiker kopiëert de inhoud van de alpha/ repository naar de bravo/ directory. Dit produceert de volgende directory structuur:

~
├── alpha
|   └── data
|       └── number.txt
└── bravo
    └── data
        └── number.txt

Er is nu een tweede Git graph in de bravo directory:

New graph created when `alpha` `cp`ed to `bravo`

Een nieuwe graph, aangemaakt door `alpha` naar `bravo` te kopiëren
      ~ $ cd alpha
~/alpha $ git remote add bravo ../bravo

De gebruiker gaat terug naar de alpha repository. bravo wordt als een remote repository van alpha aangewezen. Dit voegt een paar regels toe aan het bestand alpha/.git/config:

[remote "bravo"]
	url = ../bravo/

Deze regels houden in dat er nu een remote repository bravo is in de directory ../bravo.

Het binnenhalen (fetch) van een branch van een remote

~/alpha $ cd ../bravo
~/bravo $ printf '12' > data/number.txt
~/bravo $ git add data/number.txt
~/bravo $ git commit -m '12'
          [master 94cd04d] 12

De gebruiker gaat naar de bravo repository. Hij/zij verandert de inhoud data/number.txt in 12 en commit de verandering naar master op bravo.

`12` commit on `bravo` repository

`12` commit op de `bravo` repository

~/bravo $ cd ../alpha
~/alpha $ git fetch bravo master
          Unpacking objects: 100%
          From ../bravo
            * branch master -> FETCH_HEAD

De gebruiker gaat nu naar de alpha repository. Hij/zij haalt master van bravo binnen in alpha. Dit proces heeft vier stappen:

  • Een. Git verkrijgt de hash van de commit waar master op bravo naar wijst. Dit is de hash van de 12 commit.

  • Twee. Git maakt een lijst van alle objecten waar de 12 commit vanaf hangt: het commit object zelf, de objecten in de tree graph daarvan, de ancestor commits van de 12 commit en de objecten in hun tree graphs. Het verwijdert elk object van deze lijst dat al in de alpha object database aanwezig was. Het kopiëert de rest naar alpha/.git/objects/.

  • Drie. De inhoud van de concrete ref file alpha/.git/refs/remotes/bravo/master wordt veranderd in de hash van de 12 commit.

  • Vier. De inhoud alpha/.git/FETCH_HEAD wordt nu:

    94cd04d93ae88a1f53a4646532b1e8cdfbc0977f branch 'master' of ../bravo
    

    Dit betekent dat het meest recente fetch commando de 12 commit van master uit bravo binnenhaalde.

    `alpha` after `bravo/master` fetched

    `alpha` na het binnenhalen (fetch) van `bravo/master`

Graph property: objecten kunnen worden gekopiëerd. Dat betekent dat historie tussen repositories kan worden gedeeld.

Graph property: een repository kan remote branch refs zoals alpha/.git/refs/remotes/bravo/master opslaan. Dit betekent dat een repository lokaal de toestand van een branch in een remote repository kan bijhouden. Deze is correct op het moment van de fetch, maar zal uit de pas gaan lopen als de remote branch verandert.

Het mergen met FETCH_HEAD

~/alpha $ git merge FETCH_HEAD
          Updating d14c7d2..94cd04d
          Fast-forward

De gebruiker mergt met FETCH_HEAD. FETCH_HEAD is gewoon een andere ref. Hij verwijst naar de 12 commit, de giver. HEAD wijst naar de 11 commit, de receiver. Git doet een fast-forward merge en laat master naar de 12 commit wijzen.

`alpha` after `FETCH_HEAD` merged

`alpha` na het mergen met `FETCH_HEAD`

Het pullen van een branch van een remote

~/alpha $ git pull bravo master
          Already up-to-date.

De gebruiker pullt master van bravo in alpha. "Pull" is een verkorte schrijfwijze voor “fetch and merge FETCH_HEAD”. Git voert deze twee commando's uit en rapporteert dat master Already up-to-date is.

Het clonen van een repository

~/alpha $ cd ..
      ~ $ git clone alpha charlie
          Cloning into 'charlie'

De gebruiker gaat naar de bovengelegen directory. Dan wordt alpha naar charlie gecloned. Het clonen naar charlie heeft een vergelijkbaar resultaat als de cp die de gebruiker eerder uitvoerde om de bravo repository te produceren. Git maakt een nieuwe directory aan met de naam charlie. Het initialiseert charlie als een Git repo, voegt alpha toe als een remote met de naam origin, fetcht origin en mergt FETCH_HEAD.

Het pushen van een branch naar een uitgecheckte branch op een remote

      ~ $ cd alpha
~/alpha $ printf '13' > data/number.txt
~/alpha $ git add data/number.txt
~/alpha $ git commit -m '13'
          [master 3238468] 13

De gebruiker gaat terug naar de alpha repository. De inhoud van data/number.txt wordt veranderd in 13, en de verandering wordt gecommit naar master op alpha.

~/alpha $ git remote add charlie ../charlie

charlie wordt een remote repository van alpha gemaakt.

~/alpha $ git push charlie master
          Writing objects: 100%
          remote error: refusing to update checked out
          branch: refs/heads/master because it will make
          the index and work tree inconsistent

master wordt naar charlie gepusht.

Alle objecten die nodig zijn voor de 13 commit worden naar charlie gekopiëerd.

Op dit punt wordt het push proces afgebroken. Zoals altijd vertelt Git de gebruiker wat er mis ging. Het weigert te pushen naar een uitgecheckte branch op de remote. Daar is wat voor te zeggen. Een push zou de remote index en HEAD updaten. Dat zou verwarring veroorzaken als er iemand bezig was aan de werkkopie op de remote.

De gebruiker zou nu een nieuwe branch kunnen maken, de 13 commit erin mergen, en die branch naar charlie kunnen pushen.
Maar eigenlijk wil hij/zij een repository waar op elk willekeurig moment naartoe gepusht kan worden. Een centrale repository waarheen gepusht en waarvan gepulled kan worden, maar waar verder niemand direkt naar commit. Eigenlijk iets als een GitHub remote. Hij/zij wil een "bare repository".

Het klonen van een bare repository

~/alpha $ cd ..
      ~ $ git clone alpha delta --bare
          Cloning into bare repository 'delta'

De gebruiker gaat naar de bovengelegen directory. Hij/zij kloont delta als een bare repository. Dit is een gewone clone, maar met twee verschillen. Het config bestand geeft aan dat de repository "bare" is. En de bestanden die normaliter in de .git directory worden opgeslagen worden nu opgeslagen in de root van de repository:

delta
├── HEAD
├── config
├── objects
└── refs

`alpha` and `delta` graphs after `alpha` cloned to `delta`

`alpha` en `delta` graphs na het klonen van `alpha` naar `delta`

Het pushen van een branch naar een bare repository

      ~ $ cd alpha
~/alpha $ git remote add delta ../delta

De gebruiker gaat terug naar de alpha repository. Van delta wordt een remote repository op alpha gemaakt.

~/alpha $ printf '14' > data/number.txt
~/alpha $ git add data/number.txt
~/alpha $ git commit -m '14'
          [master cb51da8] 14

De inhoud van data/number.txt wordt veranderd in 14, en de verandering naar master gecommit op alpha.

`14` commit on `alpha`

De `14` commit op `alpha`
~/alpha $ git push delta master
          Writing objects: 100%
          To ../delta
            3238468..cb51da8 master -> master

Dan wordt master naar delta gepusht. Pushen heeft drie stappen:

  • Als eerste worden al de objecten die nodig zijn voor de 14 commit op de master branch gekopiëerd van alpha/.git/objects/ naar delta/objects/.

  • Als tweede wordt delta/refs/heads/master aangepast zodat het naar de 14 commit verwijst.

  • Als derde gaat alpha/.git/refs/remotes/delta/master wijzen naar de 14 commit (op alpha). alpha heeft nu een up-to-date weergave van de toestand bij delta.

`14` commit pushed from `alpha` to `delta`

De `14` commit, gepusht van `alpha` naar `delta`

Samenvatting

Git is gebaseerd op een graph. Bijna elk Git commando manipuleert deze graph. Richt u, om Git diepgaand te begrijpen, op de eigenschappen van deze graph, niet op workflows of commando's.

Onderzoek om meer over Git te weten te komen de .git directory. Dat is niet eng. Kijk erin. Verander de inhoud van bestanden en kijk wat er gebeurt. Creëer een commit met de hand. Probeer het uit en zie hoe grondig u een puinhoop kunt maken van een repo. Repareer hem dan.

  1. In dit geval is de hash langer dan de oorspronkelijke inhoud. Maar alle stukken inhoud die langer zijn dan het aantal karakters in een hash zullen beknopter worden weergegeven dan het origineel.

  2. Er is een kans dat twee verschillende stukken inhoud zullen hashen tot de zelfde waarde. Maar deze kans is klein.

  3. git prune verwijdert alle objecten die niet kunnen worden bereikt vanaf een ref. Als de gebruiker dit commando uitvoert kan er inhoud verloren gaan.

  4. git stash slaat alle verschillen tussen de werkkopie en de HEAD commit op op een veilige plaats. Ze kunnen later worden teruggehaald.

  5. Het rebase commando kan gebruikt worden om commits in de historie toe te voegen, te wijzigen of te verwijderen.